package edu.utexas.esel.jgraphpad.archpadplugin.model;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.xml.XMLConstants;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.jgraph.graph.DefaultGraphCell;
import org.xml.sax.SAXException;

import com.jgraph.editor.JGraphEditorResources;
import com.jgraph.pad.graph.HasToolTip;
import com.sun.org.apache.xerces.internal.impl.xs.SchemaGrammar;
import com.sun.org.apache.xerces.internal.impl.xs.XSModelGroupImpl;
import com.sun.org.apache.xerces.internal.impl.xs.XSModelImpl;
import com.sun.org.apache.xerces.internal.impl.xs.XSParticleDecl;
import com.sun.org.apache.xerces.internal.jaxp.validation.XSGrammarPoolContainer;
import com.sun.org.apache.xerces.internal.xni.grammars.Grammar;
import com.sun.org.apache.xerces.internal.xni.grammars.XMLGrammarDescription;
import com.sun.org.apache.xerces.internal.xni.grammars.XMLGrammarPool;
import com.sun.org.apache.xerces.internal.xs.XSComplexTypeDefinition;
import com.sun.org.apache.xerces.internal.xs.XSConstants;
import com.sun.org.apache.xerces.internal.xs.XSElementDeclaration;
import com.sun.org.apache.xerces.internal.xs.XSModelGroup;
import com.sun.org.apache.xerces.internal.xs.XSNamedMap;
import com.sun.org.apache.xerces.internal.xs.XSObject;
import com.sun.org.apache.xerces.internal.xs.XSObjectList;
import com.sun.org.apache.xerces.internal.xs.XSParticle;
import com.sun.org.apache.xerces.internal.xs.XSTerm;
import com.sun.org.apache.xerces.internal.xs.XSTypeDefinition;

import edu.utexas.esel.jgraphpad.archpadplugin.Utils;
import edu.utexas.esel.jgraphpad.archpadplugin.graph.ContainerCellReference;

/**
 * This class represents a tree model of an ArchPad element.
 *
 * @author svetoslavganov@mail.utexas.edu
 *
 */
public class Element extends DefaultTreeModel
        implements HasToolTip, ContainerCellReference {

	/**
	 * Defines the name for the properties file software architecture model.
	 */
	public static final String NAME_SCHEMA_LOCATION = "XSDSchemaLocation";

	private static final long serialVersionUID = 1L;

	private static final int MAX_VALUE_LENGTH = 20;

	private static Map<String, List<ElementDeclaration>>
	    sParentTypeToChildrenTypeDeclarationsMap =
		new HashMap<String, List<ElementDeclaration>>();

	private static Map<String, ElementDeclaration>
	    sElementNameToElementDeclarationMap =
	    	new HashMap<String, ElementDeclaration>();

	private DefaultGraphCell mCell;

	/**
	 * Creates a new instance with a given {@link ElementTreeNode} root.
	 *
	 * @param root The root.
	 */
	public Element(ElementTreeNode root) {
		super(root);
	}

	@Override
	public DefaultGraphCell getContainerCell() {
		return mCell;
	}

	@Override
	public void setContainerCell(DefaultGraphCell cell) {
		mCell = cell;		
	}

	public String getToolTip() {
		ElementTreeNode root = (ElementTreeNode) getRoot();
		StringBuilder html = new StringBuilder();

		html.append("<strong>");
		html.append(Utils.chopString(root.getDeclaration().getName(),
				MAX_VALUE_LENGTH));
		html.append("</strong><br>");

		for (Map.Entry<String, String> e : root.getAttributes().entrySet()) {
			html.append(e.getKey());
			html.append(": ");
			html.append(Utils.chopString(e.getValue(), MAX_VALUE_LENGTH));
			html.append("<br>");
		}

		return html.toString();
	}

	@Override
	public String toString() {
		ElementTreeNode root = (ElementTreeNode) getRoot();
		if (root != null) {
		    return root.toString();
		}
	    return null;
	}

	@Override
	public Element clone() {
		ElementTreeNode root = (ElementTreeNode) getRoot();
		ElementTreeNode clone = root.clone();
		cloneDescendents(root, clone);
		return new Element(clone);
	}

	/**
	 * Clones the descendants of <code>node</code> and adds the clones to
	 * <code>clone</code>.
	 *
	 * @param node The node.
	 * @param clone The clone.
	 */
	private void cloneDescendents(ElementTreeNode node,
			ElementTreeNode clone) {
		if (clone == null) {
			return;
		}

        for (int i = 0, count = node.getChildCount(); i < count; i++) {
        	ElementTreeNode child = (ElementTreeNode) node.getChildAt(i);
        	ElementTreeNode childClone = child.clone(); 
        	clone.add(childClone);
        	cloneDescendents(child, childClone);
        }
	}
	
	@Override
	public void insertNodeInto(MutableTreeNode newChild,
			MutableTreeNode parent, int index) {
		super.insertNodeInto(newChild, parent, index);
		addRequiredDescendents((ElementTreeNode) newChild);
	}

	/**
	 * Returns the {@link ElementDeclaration} for a given type name.
	 *
	 * @param typeName The type name.
	 * @return The {@link ElementDeclaration}.
	 */
	public static ElementDeclaration getTypeDeclaration(String typeName) {
		return sElementNameToElementDeclarationMap.get(typeName);
	}

	/**
	 * Returns the {@link ElementDeclaration}s of the children of a parent with
	 * a given type.
	 *
	 * @param parent The parent.
	 * @return The children type declarations.
	 */
	public static List<ElementDeclaration> getChildrenTypeDeclarations(
			ElementTreeNode parent) {
		return sParentTypeToChildrenTypeDeclarationsMap.get(
				parent.getDeclaration().getType());
	}

	/**
	 * Returns the number of children with a given type for a given parent.
	 *
	 * @param parent The parent.
	 * @param type The children name.
	 * @return The number of children of the given type.
	 */
	public int getChildrenOfNodeNameCount(ElementTreeNode parent,
			String name) {
		int childCount = 0;
		for (int i = 0, count = parent.getChildCount(); i < count; i++) {
    		ElementTreeNode child = (ElementTreeNode) parent.getChildAt(i);
    		if (child.getDeclaration().getName().equals(name)) {
    			childCount++;
    		}
    	}
		return childCount;
	}

	/**
	 * Adds the required descendants of a given node. Required descendants
	 * correspond to elements which minOccurance in the XSD schema is greater
	 * then zero.
	 *
	 * @param parent The parent.
	 */
	public void addRequiredDescendents(ElementTreeNode parent) {
		List<ElementDeclaration> childrenTypes =
			sParentTypeToChildrenTypeDeclarationsMap.get(
					parent.getDeclaration().getType());   

		if (childrenTypes == null) {
            return;			
		}

		for (ElementDeclaration decl : childrenTypes) {
            for (int i = 0, count = decl.mMinOccurs; i < count; i++) {
            	ElementTreeNode child = new ElementTreeNode(decl);
        	    insertNodeInto(child, parent, parent.getChildCount());
            }
		}
    }

	/**
	 * Configures the {@link Element} from a XSD schema. The
	 * file with the schema is mapped to the
	 * {@link Element#NAME_SCHEMA_LOCATION} property.
	 *
	 * @param properties The properties.
	 */
	public static void configureArchPadElementModel(
			Properties properties) {
	   	 // NOTE: This code is fragile since it relies that each type in the
		 // XSD schema is defined at the top level and is later referenced
		 // on a needed basis. Even though this is common it cannot be assumed. 
		 try {
			 SchemaFactory schemaFactory =
				 SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

		     String schemaUri = properties.getProperty(NAME_SCHEMA_LOCATION);
			 InputStream in =
				 JGraphEditorResources.getInputStream(schemaUri);
			 Schema schema = schemaFactory.newSchema(new StreamSource(in));

			 XSGrammarPoolContainer pools =(XSGrammarPoolContainer) schema;
			 XMLGrammarPool pool = pools.getGrammarPool();
			 Grammar[] grammars = pool.retrieveInitialGrammarSet(
					XMLGrammarDescription.XML_SCHEMA);
			 SchemaGrammar schemaGrammar = (SchemaGrammar) grammars[0];
			 XSModelImpl model = new XSModelImpl(
					 new SchemaGrammar[] {schemaGrammar});

			 XSNamedMap types =
				 model.getComponents(XSConstants.TYPE_DEFINITION);
			 for (int i = 0, count = types.getLength(); i < count; i++) {
				 XSObject xsObject = types.item(i);
					 processXSTypeDefinition((XSTypeDefinition) xsObject);
			 }

		 } catch (IOException ioe) {
			 String message = "Loading prent type to children type map failed!";
			 throw new RuntimeException(message, ioe);
		 } catch (SAXException se) {
			 String message = "Loading prent type to children type map failed!";
			 throw new RuntimeException(message, se);
	     }
	}
	
	/**
	 * Processes a complex type definition by putting a mapping from a parent
	 * type to the list of child types in the complex definition and a
	 * mapping from type name to type definition.
	 *
	 * @param definition The complex type definition.
	 */
	private static void processXSTypeDefinition(XSTypeDefinition definition) {
	    String typeName = definition.getName();

	    List<ElementDeclaration> children = new ArrayList<ElementDeclaration>();
	    sParentTypeToChildrenTypeDeclarationsMap.put(typeName, children);

		if (definition instanceof XSComplexTypeDefinition) {
		    XSComplexTypeDefinition complex =
		    	(XSComplexTypeDefinition) definition;     
            XSTerm dterm = complex.getParticle().getTerm();
            XSObjectList particles =((XSModelGroup) dterm).getParticles();

		    for (int i = 0, count = particles.getLength(); i < count; i++) {
		        XSParticle particle = (XSParticle) particles.item(i);
		        processXSParticle(children, particle);
			}
		}
	}

	/**
	 * Processes an {@link XSParticle} and adds a {@link TypeDefinition} for
	 * the particle to a <code>declarations</code> list.
	 *
	 * @param declarations The declarations.
	 * @param particle The particle.
	 */
	private static void processXSParticle(List<ElementDeclaration> declarations,
			XSParticle particle) {
	    XSTerm term = particle.getTerm();
	    if (term instanceof XSElementDeclaration) {
	        XSElementDeclaration decl = (XSElementDeclaration) term;
	        String name = decl.getName();

	        ElementDeclaration typeDecl = sElementNameToElementDeclarationMap.get(name);
	        if (typeDecl == null) {
	            String type = decl.getTypeDefinition().getName();
	            int minOccurs = particle.getMinOccurs();
	            int maxOccurs = particle.getMaxOccurs();
	            typeDecl = new ElementDeclaration(name, type, minOccurs,
	            		maxOccurs);
	            sElementNameToElementDeclarationMap.put(name, typeDecl);
	        }
	        declarations.add(typeDecl);
	    } else if (term instanceof XSModelGroupImpl) {
	    	XSModelGroupImpl group = (XSModelGroupImpl) term;
	    	for (XSParticleDecl gParticle : group.fParticles) {
	    	   processXSParticle(declarations, gParticle);
	    	}
	    }
	}

	/**
	 * Auxiliary class for storing information about an element.
	 */
	public static class ElementDeclaration {
		private final String mElementName;
		private final String mElementType;
		private final int mMinOccurs;
		private final int mMaxOccurs;

		/**
		 * Creates a new instance given node name, node type, min occurances,
		 * and max occurances.
		 *
		 * @param xmlNodeName The node name (XML element name)
		 * @param xmlTypeName The node type (XML element type)
		 * @param minOccurs The minimum occurances of the element.
		 * @param maxOccurs The maximum occurances of the element.
		 */
		public ElementDeclaration(String xmlNodeName, String xmlTypeName,
				int minOccurs, int maxOccurs) {
			mElementName = xmlNodeName;
			mElementType = xmlTypeName;
			mMinOccurs = minOccurs;
			mMaxOccurs = maxOccurs;
		}

		/**
		 * Gets the name of the element in the SXD schema for the model.
		 *
		 * @return The name.
		 */
		public String getName() {
			return mElementName;
		}

		/**
		 * Gets the type of the element in the SXD schema for the model.
		 *
		 * @return The type.
		 */
		public String getType() {
			return mElementType;
		}

		/**
		 * Gets the minimum occurrence of the element in the XSD schema of
		 * the model.
		 *
		 * @return The minimum occurrences.
		 */
		public int getMinOccurs() {
			return mMinOccurs;
		}

		/**
		 * Gets the maximum occurrence of the element in the XSD schema of
		 * the model.
		 *
		 * @return The maximum occurrences.
		 */
		public int getMaxOccurs() {
			return mMaxOccurs;
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append("ElementName: ");
			builder.append(mElementName);
			builder.append(" ElementType: ");
			builder.append(mElementType);
			builder.append(" MinOccurs: ");
			builder.append(mMinOccurs);
			builder.append(" MaxOccurs: ");
			builder.append(mMaxOccurs);
			return builder.toString(); 
		}
	}
	
	/**
	 * Writes element starting from the root node to XML using SAX.
	 *
	 * @param handler The handler used for writing.
	 * @throws SAXException If an error occurs while writing the node.
	 * 
     * @see ElementTreeNode#writeToXMLViaSAX(TransformerHandler)
	 */
	public void writeToXMLViaSAX(TransformerHandler handler) throws SAXException {
		((ElementTreeNode) getRoot()).writeToXMLViaSAX(handler);
	}
}
